VAPID Library
是使用「非對稱式加密的方式」確保傳送推播訊息到瀏覽器供應商的push server是「來自我自己的server」。
也就是說它會產生一組公私鑰對,私鑰儲存在我自己的server。公鑰則是當已訂閱的用戶發佈新貼文時,連同「API endpoint」和「一些金鑰交換的資訊」一起傳送出去(這段後面會說明),push server就會根據這些資訊來驗證是否這是從我的server發送的推播訊息。
首先,要先在我的backend server(也就是Firebase Cloud Functions)中新增VAPID package:
npm install --save web-push
接著在package.json裡添加執行這個modules的scripts:
"scripts": {
"web-push": "web-push"
}
之後在terminal中我就可以透過執行npm run web-push generate-vapid-keys
來產生一組公私鑰對:
BTW 請好好管理自己的私鑰,千萬不要外流
接著在app.js的configurePushSub()繼續完成昨天還沒完成的設定:
function configurePushSub() {
if(!('serviceWorker' in navigator)) {
return;
}
var reg;
navigator.serviceWorker.ready.then(function(swreg) {
reg = swreg;
return swreg.pushManager.getSubscription();
}).then(function(sub) {
if(sub === null) {
// Create a new subscription
var vapidPublicKey = 'BDIOql6aKK-00AGzVKggeN9LSpjGd2golLzuiCvmUG0NAIa3wi-FmG17HElLHhXtzQBQQ9faZmJ2MWW87VI8bgg';
var convertedVapidPublicKey = urlBase64ToUint8Array(vapidPublicKey);
return reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidPublicKey
});
} else {
// We have a subscription
}
}).then(function(newSub) {
return fetch('https://trip-diary-f56de.firebaseio.com/subscriptions.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(newSub)
})
}).then(function(res) {
if(res.ok) {
displayConfirmNotification();
}
}).catch(function(err) {
console.log(err);
})
}
在用戶按下訂閱鈕後,添加剛剛產生的public key到pushManager.subscribe()中。不過這裡要先進行編碼的轉換,因為產生的公鑰是base64編碼,但subscribe()接受的是今鑰編碼是一個Uint8陣列,這裡直接使用gist上已經寫好的function進行轉換(我把這個function加到utility.js中)。
瀏覽器供應商push server接收到這個訂閱訊息後,會回傳剛剛所說的「API Endpoint」+「金鑰交換」資訊,我將這些資訊儲存在後台firebase資料庫中,最後我才將顯示「成功訂閱」的通知。
firebase儲存的訂閱資訊:
再來回到Firebase Cloud Funcitons的StorePostData() API。當用戶發佈新貼文時,會呼叫這個API並儲存到後台資料庫中,不過現在除了儲存這些貼文資訊,我現在還要將push message傳送到前面從push server取得的API Endpoint
。
來看一下該怎麼做:
var webpush = require('web-push');
exports.storePostData = functions.https.onRequest((request, response) => {
cors(request, response, function() {
admin.database().ref('posts').push({
id: request.body.id,
title: request.body.title,
location: request.body.location,
image: request.body.image
}).then(function() {
webpush.setVapidDetails('mailto:j84077200345@gmail.com', 'BDIOql6aKK-00AGzVKggeN9LSpjGd2golLzuiCvmUG0NAIa3wi-FmG17HElLHhXtzQBQQ9faZmJ2MWW87VI8bgg', 'JxB633wEwprQT3hahwrNPoimHshPRj0Kd9OK11IXlQ8');
return admin.database().ref('subscriptions').once('value');
}).then(function(subscriptions) {
subscriptions.forEach(function(sub) {
var pushConfig = {
endpoint: sub.val().endpoint,
keys: {
auth: sub.val().keys.auth,
p256dh: sub.val().keys.p256dh
}
};
webpush.sendNotification(pushConfig, JSON.stringify({title: '新貼文', content: '有新增的貼文!!'})).catch(function(err) {
console.log(err);
});
});
response.status(201).json({message: 'Data Stored', id: request.body.id});
}).catch(function(err) {
response.status(500).json({error: err});
})
});
});
首先一樣要導入web-push套件,使用webpush.setVapidDetails()來設定server的金鑰資訊(第一個參數為server的對外信箱,第二個為public key,最後是private key)。
設定完成後,要先從資料庫裡獲取目前所有訂閱用戶的資訊,這樣才可以針對每個有訂閱的用戶來發送推播通知。所以這裡使用webpush.sendNotification() method將「資料庫中的每一筆用戶訂閱資訊(endpoint+keys)」和「要推送的訊息(json format)」push到瀏覽器供應商的push server。
目前我們的server已經可以push messages到瀏覽器供應商的push server了。現在準備要在service worker監聽這個「push事件」,當監聽到這個push event,service worker就可以將有新貼文的訊息通知給所有訂閱用戶,就算今天用戶沒有開啟PWA。
在sw.js中監聽push事件:
self.addEventListener('push', function(event) {
console.log('Push Notification', event);
var data = {title: 'New!', content: 'Something new!'};
if(event.data) {
data = JSON.parse(event.data.text());
}
var options = {
body: data.content,
icon: '/src/images/icons/app-icon-96x96.png',
badge: '/src/images/icons/app-icon-96x96.png'
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
這裡監聽到的事件,裡面的data屬性就是我剛剛在server中在sendNotification()輸入的的json資料(也就是{title: '新貼文', content: '有新增的貼文!!'}
)。
接著跟前面實作「顯示通知」時一樣,會設定一些選項(body、icon、badge...)。最後必須要取得正在browser運行的已註冊service worker(self.registration
),並呼叫showNotification()將要顯示的選項設定和內容傳入。
看一下當我新增一篇貼文時,顯示通知的結果:
最後我要繼續完成之前在實作「顯示通知」時,用戶點擊通知時互動的部分。回到service worker中監聽notificationclick事件,現在要實作當用戶點擊通知時,會導向或開啟我的PWA頁面。
首先使用clients.match()取得所有開啟的視窗或頁面,它會回傳一個promise。
根據回傳的陣列(我把它命名為clis)使用js的find函式確認是否有由這個service worker管理的視窗(也就是我的PWA頁面是否開啟的)。有的話(不等於undefinded)就navigate到這段url(notification.data.url
),沒有則open一個新window開啟我們的PWA。
self.addEventListener('notificationclick', function(event) {
var notification = event.notification;
var action = event.action;
console.log(notification);
if(action === 'confirm') {
console.log('Confirm was chosen');
notification.close();
} else {
console.log(action);
event.waitUntil(
clients.matchAll().then(function(clis) {
var client = clis.find(function(c) {
return c.visibilityState === 'visible';
});
if(client !== undefined) {
client.navigate(notification.data.url);
client.focus();
} else {
clients.openWindow(notification.data.url);
}
notification.close();
})
);
}
});
說明一下notification.data.url
是怎麼來的?
我後來在server傳遞的push messages中又加入了openUrl{title: '新貼文', content: '有新增的貼文!!', openUrl: '/'}
,代表著用戶點擊通知時要導向的頁面。
這樣service worker監聽到這個push event時,就可以在選項中將這個url設定到data這個屬性。
var options = {
body: data.content,
icon: '/src/images/icons/app-icon-96x96.png',
badge: '/src/images/icons/app-icon-96x96.png',
data: {
url: data.openUrl
}
};
最後當**用戶點擊事件(notificationclick)**發生時,service worker就可以從notification.data.url知道要導向的url是什麼。
推播通知的實作就到這啦!!
Day24 結束!!